# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2012  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

require 'redmine/scm/adapters/abstract_adapter'

module Redmine
  module Scm
    module Adapters
      class PlasticAdapter < AbstractAdapter

        class PlasticBranch < Branch 
          attr_accessor :is_selected
        end

        class Revision < Redmine::Scm::Adapters::Revision
        end
        
        # Plastic executable name
        CM_BIN = Redmine::Configuration['scm_plastic_command'] || "cm"

        DEFAULT_SEPARATOR="def#_#sep"

        class << self
          def client_command
            @@bin    ||= CM_BIN
          end

          def sq_bin
            @@sq_bin ||= shell_quote_command
          end

          def client_version
            @@client_version ||= (scm_command_version || [])
          end

          def client_available
            !client_version.empty?
          end	

          def scm_command_version
            scm_version = scm_version_from_command_line.dup
            if scm_version.respond_to?(:force_encoding)
              scm_version.force_encoding('ASCII-8BIT')
            end
            #scm_version.is_a?(Array) ? scm_version.join('.') : scm_version.to_s.strip
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
              m[2].scan(%r{\d+}).collect(&:to_i)
            end            
          end

          def scm_version_from_command_line
            shellout("#{sq_bin} version") { |io| io.read }.to_s
          end
        end

        
        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
          super
          logger.debug { "plastic initialize url:#{url}, root_url:#{root_url}" }
          #@root_url = root_url.blank? ? 'localhost:8087' : root_url
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
        end

        def path_encoding
          @path_encoding
        end

        # get info about the plasticscm repository
        # used by AbstractAdapter#retrieve_root_url
        # Info contains root_url and last_revision info
        def info
          return nil
        end

        def branches(additional_branches=nil,use_additional_branches=false)
          if @branches && additional_branches.nil?
            logger.debug { "<plastic> branches @branches:[#{@branches}]" }
            return @branches
          end
          logger.debug { "<plastic> branches fetch_plastic_branch"}
          @branches = []
          selected_branch_name = default_branch
          unless selected_branch_name.blank?
            selected_branch = fetch_plastic_branch(selected_branch_name)
            selected_branch.is_selected = true
            @branches << selected_branch
          end
          if use_additional_branches
            unless additional_branches.blank?
              br_names = additional_branches.to_s.split(%r{[;:]}).select {|n| !n.blank?}
              br_names.each do |br|
                plastic_branch = fetch_plastic_branch(br)
                @branches << plastic_branch
              end
            end
          else
            @branches += fetch_plastic_branches
          end
          @branches.sort!
        end

        # return string type (used in RepositoriesController#find_project_repository)
        def default_branch(branch_name=nil)
          logger.debug {"<plastic> default_branch branch_name:#{branch_name}" } unless branch_name.nil? || branch_name.empty?
          return @default_branch if @default_branch && branch_name.blank?
          plastic_branch = fetch_plastic_branch(branch_name)
          @default_branch = plastic_branch.blank? ? '/main' : plastic_branch.chomp
          return @default_branch
        end
        
        # used by branches and default_branch
        # return PlasticBranch for branch_name (update changeset) 
        def fetch_plastic_branch(branch_name)
          unless branch_name.nil?
            return nil if branch_name.empty?
            parts = branch_name.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
            search_dir = parts[0..-2].join('/')
            search_name = parts[-1]
            cmd_args = %w{find}
            cmd_args << "branch"
            cmd_args << "where"
            cmd_args << %Q{"name='#{search_name}'"}
            if !search_dir.blank?
              cmd_args << "and"
              cmd_args << %Q{"parent='#{with_leading_slash(search_dir)}'"} 
            end
            cmd_args << %Q{"on repository '#{url}'"}
            cmd_args << %Q{--nototal} 
            delimiter = '_|_'
            cmd_args << %Q{--format="{name}#{delimiter}{id}#{delimiter}{changeset}#{delimiter}{parent}#{delimiter}"}
            logger.debug { "<plastic> fetch_plastic_branch cmd_args=>#{cmd_args}" }
            pscm_cmd(cmd_args) do |io|
              io.each_line do |line|
                name, id, changeset, parent = line.split(delimiter)
                logger.debug { "<plastic> fetch_plastic_branch name:[#{name}], id:[#{id}], changeset:[#{changeset}], parent:[#{parent}]" }
                unless name.blank? && name != default_branch
                  plastic_branch = PlasticBranch.new(name)
                  plastic_branch.revision = changeset
                  plastic_branch.scmid = id
                  return plastic_branch
                end
              end
            end
          end
        end
        private :fetch_plastic_branch
        
        # used by branches
        # return PlasticBranch Array for branch_name (update changeset) 
        def fetch_plastic_branches
          plastic_branches=[]
          cmd_args = %w{find}
          cmd_args << "branch"
          cmd_args << %Q{"on repository '#{url}'"}
          cmd_args << %Q{--nototal} 
          delimiter = '_|_'
          cmd_args << %Q{--format="{name}#{delimiter}{id}#{delimiter}{changeset}#{delimiter}{parent}#{delimiter}"}
          logger.debug { "<plastic> fetch_plastic_branches cmd_args=>#{cmd_args}" }
          pscm_cmd(cmd_args) do |io|
            io.each_line do |line|
              name, id, changeset, parent = line.split(delimiter)
              logger.debug { "<plastic> fetch_plastic_branches name:[#{name}], id:[#{id}], changeset:[#{changeset}], parent:[#{parent}]" }
              unless name.blank?
                plastic_branch = PlasticBranch.new(name)
                plastic_branch.revision = changeset
                plastic_branch.scmid = id
                plastic_branches << plastic_branch
              end
            end
          end
          return plastic_branches
        end
        private :fetch_plastic_branches
        
        def tags
          return @tags if @tags
          cmd = %Q{#{self.class.sq_bin} find label "on repository '#{url}'" --nototal --format="{name}"}
          shellout(cmd) do |io|
            @tags = io.readlines.sort!.map{|t| t.strip}
          end
        rescue ScmCommandAborted
          nil
        end

        #
        # cm ann support only current active branch  (= home) path
        #
        #def annotate(path, identifier=nil)
        #  
        #  return nil if root_url.nil? || root_url.empty?
        #  cmd_args = %w|annotate|
        #  cmd_args << path
        #  blame =  Annotate.new
        #  pscm_cmd(cmd_args) { |io| io.binmode; content = io.read }
        #  blame
        #rescue ScmCommandAborted
        #  nil          
        #end

        # Returns the entry identified by path and revision identifier
        # or nil if entry doesn't exist in the repository
        #
        # add options = {:report_last_commit => false} when call entries
        def entry(path=nil, identifier=nil)
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
          search_path = parts[0..-2].join('/')
          search_name = parts[-1]
          if search_path.blank? && search_name.blank?
            # Root entry
            Entry.new(:path => '', :kind => 'dir')
          else
            # Search for the entry in the parent directory
            es = entries(search_path, identifier,
                         options = {:report_last_commit => false})
            es ? es.detect {|e| e.name == search_name} : nil
          end
        end
        
        # Returns an Entries collection
        # or nil if the given path doesn't exist in the repository
        #
        # default format string is: (cm ls)
        # "{<size>,10} {<date>:dd/MM/yyyy} {<date>:HH:mm}
        #  {<type>,-6} {<location>,-12} {<co>,-5} {name}
        #  {symlinktarget}"
        def entries(path=nil, identifier=nil, options={})
          logger.debug {"<plastic> entries path:#{path}, identifier:#{identifier}, options:#{options.inspect}"}
          entries = Entries.new
          
          path ||= ''
          path_utf8 = scm_iconv(@path_encoding, 'UTF-8', path)          
          cmd_args = %w|ls|
          cmd_args << %Q{"#{with_leading_slash(path_utf8)}"}
          cmd_args << %Q{--selector="rep '#{url}' path '/' branch '#{identifier || default_branch}'"}
          format = []
          format << "{<size>,10}"
          format << "{<date>:yyyy/MM/dd HH:mm:ss}"
          format << "{type}"
          format << "{changeset}"
          format << "{<location>,-12}"
          format << "{owner}"
          format << "{name}"
          separator = '@'
          cmd_args << %Q{--format="#{format.join(separator)}"}
          logger.debug {"<plastic> entries cmd_args=>#{cmd_args}" }
          pscm_cmd(cmd_args) do |io|
            io.each_line do |line|
              e = line.chomp.to_s.split('@')
              logger.debug  {"entries: e.size:#{e.size}, #{e}"}
              # 4 depends on format
              if e.size == 7
                size = e[0]
                date = e[1]
                type = e[2].strip
                changeset = e[3]
                lastrev = e[4].split('#')[1].strip
                owner = e[5]
                name = e[6]
                if name.respond_to?(:force_encoding)
                  name.force_encoding(@path_encoding)
                end
                full_path = path_utf8.empty? ? name : with_leading_slash(path_utf8) + with_leading_slash(name)
                name_utf8 = scm_iconv('UTF-8', @path_encoding, name)
                kind = (type == "dir") ? 'dir' : (type == "txt") ? 'text' : 'file'
                next if (type == "dir") and (name == ".")
                entries << Entry.new({:name => name_utf8,
                  :path => full_path,
                  :kind => kind,
                  :size => (type == "dir") ? nil : size,
                  :lastrev => Revision.new({:identifier => changeset, :time => Time.parse(date), :author => owner })
                  }) unless entries.detect {|entry| entry.name == name}
              end
            end
          end
          entries.sort_by_name
        rescue ScmCommandAborted
          nil
        end

        #
        #
        # identifier_from: changesetid
        # identifier_to: branch_name
        # options: include, excludes
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
          logger.debug {"<plastic> revisions path=#{path}, from=#{identifier_from}, to=#{identifier_to}, options=#{options.inspect}"}
          revs = Revisions.new
          # setup branches from input params
          brs ||= {} 
          if identifier_from || identifier_to
            if identifier_from
              # GetBranchForChangesetCommand
              branch_for_changeset = nil
              unless branch_for_changeset.blank?
                brs[branch_for_changeset] ||= {}
                brs[branch_for_changeset][:changeset_from] = identifier_from
              end
            end
            if identifier_to
              brs[identifier_to] = {}
            end
          else
            unless options[:includes].blank?
              options[:includes].each do |br_name, value_name|
                logger.debug {"<plastic> revisions options[:includes]=>#{br_name.inspect},#{value_name.inspect}"}
                brs[br_name] ||= {}
                brs[br_name][:changeset_included] = value_name[:last_changeset] unless value_name.blank?                  
              end              
            end
            unless options[:excludes].blank?
              options[:excludes].each_key do |br_name, value_name|
                logger.debug {"<plastic> revisions options[:excludes]=>#{br_name.inspect},#{value_name.inspect}"}
                brs[br_name] ||= {}
                brs[br_name][:changeset_excluded] = value_name[:last_changeset] unless value_name.blank?                 
              end              
            end
          end
          logger.debug {"<plastic> revisions brs=#{brs.inspect}"}
          # DetailedHistoryCommand
          brs.each do |br_name, value_name|
            logger.debug {"<plastic> revisions each brs =>#{br_name.inspect},#{value_name.inspect}"}
            #parts = br_name.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
            #search_dir = parts[0..-2].join('/')
            #search_br_name = parts[-1]
            cmd_args = %w|find|
            cmd_args << "changeset"
            cmd_args << "where"
            cmd_args << %Q|"branch='#{br_name}'"|
            if value_name.include? :changeset_from
              cmd_args << "and"
              cmd_args << %Q|"changesetid >= #{value_name[:changeset_from]}"|
            end
            if value_name.include? :changeset_excluded
              cmd_args << "and"
              cmd_args << %Q|"changesetid > #{value_name[:changeset_excluded]}"|
            end
            if value_name.include? :changeset_included
              cmd_args << "and"
              cmd_args << %Q|"changesetid <= #{value_name[:changeset_included]}"|
            end            
            cmd_args << "on"
            cmd_args << "repositories"
            cmd_args << %Q{"'#{url}'"}
            cmd_args << %Q{--dateformat="yyyy-MM-dd HH:mm:ss"}
            cmd_args << "--xml"
            logger.debug { "<plastic> revisions cmd_args:[#{cmd_args}]" }
            pscm_cmd(cmd_args) do |io|
              logger.debug { "<plastic> revisions check1" }
              output = io.read
              if output.respond_to?(:force_encoding)
                output.force_encoding('UTF-8')
              end
              logger.debug { "<plastic> revisions check2" }
              begin
                doc = parse_xml(output)
                logger.debug { "<plastic> revisions check3" }
                each_xml_element(doc['PLASTICQUERY'], 'CHANGESET') do |changeset|
                  logger.debug "<plastic> revisions changeset:#{changeset}"
                  #changeset.elements['CHANGESETID'].text
                  changesetid = changeset['CHANGESETID']['__content__']
                  #
                  # GetChangesetRevisionsCommand
                  #
                  files = update_files(changesetid)
                  logger.debug "<plastic> revisions file cs:#{changesetid} ... files:#{files.inspect}"
                  attributes = get_attribute_on_revision(changeset, changesetid, files)
                  revision = Revision.new(attributes)
                  if block_given?
                    yield revision
                  else
                    revs << revision
                  end
                end
              rescue
              end
            end         
          end          
          revs
        rescue ScmCommandAborted => e
          err_msg = "cm find errors: #{e.message}"
          logger.error(err_msg)
          if block_given?
            raise CommandFailed, err_msg
          else
            revs
          end  
        end


        def update_files(csid, files = nil)
          result = files || []
          result = get_changeset_revisions_command(csid, result)
          result
        end

        def get_changeset_revisions_command(cs_version, files = nil)
          result = files || []
          cmd_args = %w|find|
          cmd_args << "revisions"
          cmd_args << "where"
          cmd_args << %Q{"changeset=#{cs_version}"}
          cmd_args << %Q{"on"}
          cmd_args << %Q{"repositories"}
          cmd_args << %Q{"'#{url}'"}
          cmd_args << "--xml"
          logger.debug { "<plastic> update_files_for_revision cmd_args:[#{cmd_args}]" }
          pscm_cmd(cmd_args) do |io|
            output = io.read
            if output.respond_to?(:force_encoding)
              output.force_encoding('UTF-8')
	          end
            begin
              doc = parse_xml(output)
              each_xml_element(doc['PLASTICQUERY'], 'REVISION') do |rev|
                filepath = rev['PATH']['__content__']
                fileaction = rev['PARENT']['__content__'] == "-1" ? "A" : "C"
                logger.verbose { "<plastic> update_files_for_revision ... action:#{fileaction}, path:#{filepath}" }
                logger.verbose { "<plastic> update_files_for_revision ... rev:#{rev}" }
                # rev.elements['PATH'].text
                #(rev.elements['PARENT'].text == "-1") ? "A" : "C"
                result << {:action=>fileaction, :path=> filepath}
              end
            rescue
            end
          end
          result          
        rescue ScmCommandAborted => e
          err_msg = "cm find errors: #{e.message}"
          logger.error(err_msg)
          if block_given?
            raise CommandFailed, err_msg
          else
            result
          end
        end

        def get_attribute_on_revision(changeset, changesetid, files)
          parents = [ changeset['PARENT']['__content__'] ]
          attribute = {
            :identifier => changesetid,
            :scmid => changesetid, #changeset['ID']['__content__'],            #changeset.elements['ID'].text,
            :author => changeset['OWNER']['__content__'],           #changeset.elements['OWNER'].text,
            :time => Time.parse(changeset['DATE']['__content__']), #Time.parse(changeset['DATE'].text),
            :message => changeset['COMMENT']['__content__'],     #changeset.elements['COMMENT'].text,
            :paths => files,
            :branch => changeset['BRANCH']['__content__'],       #changeset.elements['BRANCH'].text,
            :parents => parents                                  #changeset.elements['PARENT'].text.to_a
          }
        end

        def lastrev(path, rev)
          return nil if path.brank?
          cmd_args =%w|ls|
          cmd_args << path
          lines = []
          pscm_cmd(cmd_args) { |io| lines = io.readlines }
          begin
              id = lines[0].split[1]
              author = lines[1].match('Author:\s+(.*)$')[1]
              time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])

              Revision.new({
                :identifier => id,
                :scmid      => id,
                :author     => author,
                :time       => time,
                :message    => nil,
                :paths      => nil
                })
          rescue NoMethodError => e
              logger.error("The revision '#{path}' has a wrong format")
              return nil
          end
        rescue ScmCommandAborted
          nil
        end

        # 
        def diff(path, rev, rev_to=nil)
          if root_url.blank?
            raise ScmCommandAborted.new("Workspace on disk cann't blank for diff... Check the settings for the PlasticScm Repository(admin only)")
          end
          logger.info {"plastic diff path:#{path} rev:#{rev} rev_to:#{rev_to}" }
          path ||= ''
          cmd_args = %w|diff|
          if rev_to
            
            cmd_args << %Q|"cs:#{rev_to}"|
            cmd_args << %Q|"cs:#{rev}"|
          else
            cmd_args << %Q|"cs:#{rev}"|
          end
          diff = []
          pscm_cmd(cmd_args) do |io|
            io.each_line do |line|
              logger.info { "plastic diff line: #{line}" }
              diff << line
            end
          end
          logger.info { "plastic diff: #{diff}" }
          diff
        rescue ScmCommandAborted => e
          err_msg = "cm diff error: #{e.message}"
          raise CommandFailed.new(err_msg)
        end

        # not yet support (needs workspace or cm command )
        # 
        def cat(path, identifier=nil)
          logger.debug {"plastic cat path:#{path} id:#{identifier} " }
          return nil
        end        

        private

			  # Returns the relative url of the repository
			  # Eg: root_url = file:///var/svn/foo
			  #     url      = file:///var/svn/foo/bar
			  #     => returns /bar
			  def relative_url
			    @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(@root_url)}", Regexp::IGNORECASE), '')
			  end
			  
        def pscm_cmd(args, options = {}, &block)
          full_args = []
          full_args += args
          ret = shellout(
                   self.class.sq_bin + ' ' + full_args.map { |e| e.to_s }.join(' '),
                   options,
                   &block
                   )
          if $? && $?.exitstatus != 0
            raise ScmCommandAborted, "plastic exited with non-zero status: #{$?.exitstatus}"
          end
          ret
        end
        private :pscm_cmd
        
        def pscm_cmd_with_workspace(args, options = {}, &block)
          if root_url.blank?
            pscm_cmd(args, options, &block)
          else
            cd_path_to_workspace = %w|cd|
            cd_path_to_workspace << "/D" if Redmine::Platform.mswin?
            cd_path_to_workspace << root_url
            cd_path_to_workspace << "&&"
            ret = shellout(
                     cd_path_to_workspace.map { |e| e.to_s }.join(' ') + ' ' +
                     self.class.sq_bin + ' ' + full_args.map { |e| e.to_s }.join(' '),
                     options,
                     &block
                     )
            if $? && $?.exitstatus != 0
              raise ScmCommandAborted, "plastic exited with non-zero status: #{$?.exitstatus}"
            end
            ret
          end
        end
        private :pscm_cmd_with_workspace

        # Helper that iterates over the child elements of a xml node
        # MiniXml returns a hash when a single child is found
        # or an array of hashes for multiple children
        def each_xml_element(node, name)
          if node && node[name]
            if node[name].is_a?(Hash)
              yield node[name]
            else
              node[name].each do |element|
                yield element
              end
            end
          end
        end
                        
      end
    end
  end
end
